######################################## 文件 IO ######################################## 本部分讲述的内容是不带缓冲的、以文件描述符为基准的 IO,这与标准 IO 库中带有缓冲的、以 FILE* 为基准的 IO 形成了区分 文件描述符就是一个非负整数,文件描述符的范围为 0~OPEN_MAX-1,现在 OPEN_MAX 为 63(也就是说进程最多能打开 64 个文件)。当进程创建时,系统自动为其打开三个文件描述符,这三个文件描述符分别是: +------------+---------------+----------+ | 文件描述符 | 宏 | 解释 | +============+===============+==========+ | 0 | STDIN_FILENO | 标准输入 | +------------+---------------+----------+ | 1 | STDOUT_FILENO | 标准输出 | +------------+---------------+----------+ | 2 | STDERR_FILENO | 标准错误 | +------------+---------------+----------+ 另外,当打开一个文件时,系统会使用当前可用的最低文件描述符,这样,我们可以通过先关闭 STDOUT_FILENO,然后再打开文件的形式将标准输出重定向到文件中 内核使用三个数据结构用来描述进程打开的文件:进程表项、文件表项、 v 节点表项 - v 节点表项是文件在物理磁盘中的索引。当文件第一次被打开时,系统将其载入内存 - 文件表项由内核维护,是进程共享的,包含的三个字段用来描述文件打开的状态:文件状态标志、当前文件偏移量、v 节点指针 - 进程表项是进程私有的,其将文件描述符映射到文件指针上,而文件指针指向了文件表项 dup **************************************** dup 函数用于复制一个已有的文件描述符,其函数原型为: .. code-block:: c int dup(int fd); int dup2(int fd1, int fd2); dup 函数复制 fd 并将复制后的文件描述符返回。dup2 将 fd1 的描述符复制到 fd2,如果 fd2 已经打开,则先关闭 fd2,如果 fd1 == fd2,则直接返回。 复制后的文件描述符和以前的文件描述符只是文件描述符一样,其指向的文件表项相同 /dev/fd **************************************** .. note:: 本文的场景为 Linux,Unix 暂不考虑 在 Linux 中提供了 /dev/fd 目录,此目录中的目录项为当前进程打开的所有文件描述符。由于 Linux “一切皆文件的思想”,此目录中的目录项是指向底层物理文件的符号链接,直接打开 /dev/fd 中的目录项相当于 dup 函数,且新文件描述符的属性与原有描述符无关(这点和 Unix 不同) 另外,Linux 还创建了 /dev/stdin, /dev/stdout, /dev/stdout 三个符号链接。在终端中可以很方便地使用它们。例如将消息发送到 stderr :: echo 'hello' > /dev/stderr 或是 :: echo 'hello' > /dev/fd/3 .. tip:: C++ 可以通过打开 /dev/fd 的形式读写文件,例如 :: std::ofstream os("/dev/fd/1"); os<<"hello"; 会将消息发送到 stdout 打开文件 **************************************** 调用 open 或者 openat 可以打开一个文件: .. code-block:: c int open(const char* path, int oflag, ... /* mode_t mode */); int openat(int fd, const char* path, int oflag, ... /* mode_t mode */); fd 用来代指 path 的起始路径,当 fd 具有 AT_FDCWD 属性时,openat 和 open 函数并无差距。另外, **open 和 openat 总是选择当前可用的最小的文件描述符** .. note:: 常量 _POSIX_NO_TRUNC 决定了当文件名太长时函数出错还是截断路径。一般而言,现代操作系统允许的最长路径为 255 个字符 oflag 指定了打开文件时的行为,主要有: +-------------+-------------------------------------------------------------------------------------------------------------+ | 标志 | 作用 | | O_RDONLY | 只读打开 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_WRONLY | 只写打开 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_RDWR | 读写打开 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_EXEC | 只执行打开 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_APPEND | 每次写时追加到文件末端 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_CLOEXEC | 把 FD_CLOEXEC 设置为文件描述符标志 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_CREAT | 文件不存在时则创建 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_DIRECTORY | 若 path 不是路径则报错 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_EXCL | 如果同时指定了 O_CREAT 而文件又存在,则报错,否则创建新文件。此操作可用来确定文件是否存在,且是一个原子操作 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_NOCTTY | 若 path 指向的是终端设备,则不将此设备作为此进程的控制终端 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_NONBLOCK | 将文件设置为非阻塞模式 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_SYNC | 同步写文件内容和文件属性 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_TRUNC | 截断文件 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_TTY_INIT | | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_DSYNC | 同步写文件内容 | +-------------+-------------------------------------------------------------------------------------------------------------+ | O_RSYNC | 使所有以 fd 为参数的 write 操作等待至对文件的同一部分写操作完成 | +-------------+-------------------------------------------------------------------------------------------------------------+ 另外,当指定 O_CREAT 选项时,mode 参数必须指定,此参数用来说明新建文件的权限位 关闭文件可以使用 close: .. code-block:: c int close(int fd); - 当关闭文件时会自动释放进程在此文件上持有的记录锁 - 当进程终止时,内核会自动关闭它打开的文件 .. note:: 很多程序利用上述第二个特性自动关闭文件,但是这里并不建议,因为当你的程序被脚本循环调用时可能会导致占用大量文件句柄 fcntl **************************************** fcntl 用来更改已打开文件的属性: .. code-block:: c int fcntl(int fd, int cmd, ...); fcntl 最后一个参数有两种形式,要么是 int,要么是一个指向结构体的指针。当为 int 时含义即为 oflags 根据 cmd 参数,fcntl 具有以下能力: +---------------------+-------------------------+------------------+ | 功能 | getter | setter | +=====================+=========================+==================+ | 复制文件描述符 | F_DUPFD/F_DUPFD_CLOEXEC | | +---------------------+-------------------------+------------------+ | 设置文件描述符 | F_GETFD | F_SETFD | +---------------------+-------------------------+------------------+ | 设置文件标志 | F_GETFL | F_SETFL | +---------------------+-------------------------+------------------+ | 获取异步 I/O 所有权 | F_GETOWN | F_SETOWN | +---------------------+-------------------------+------------------+ | 设置记录锁 | F_SETLK | F_SETLK/F_SETLKW | +---------------------+-------------------------+------------------+ 对于 F_DUPFD 而言,返回的文件描述符是大于/等于第三个参数的最小值,新的文件描述符与原文件描述符的描述符标志不同,其 F_CLOEXEC 标志被清除 lseek **************************************** 程序持有的每个文件句柄都有一个与其相关联的文件偏移量属性,其代表了下次读写文件时开始的位置。可以使用 lseek 改变此属性: .. code-block:: c off_t lseek(int fd, off_t offset, int whence); 此函数会将文件指针定位到 whence + offset 的位置,whence 的取值为: +----------+------------------+ | 取值 | 含义 | +==========+==================+ | SEEK_SET | 文件开头 | +----------+------------------+ | SEEK_CUR | 文件当前偏移位置 | +----------+------------------+ | SEEK_END | 文件末尾 | +----------+------------------+ 若 lseek 成功,则返回新的文件偏移位置,否则返回 -1。对于管道、FIFO、套接字这些无法设置偏移量的文件而言,还会置 error = ESPIPE。另外,文件偏移位置: - 某些文件允许负的偏移位置 - 文件偏移量允许超过文件末尾 在第二种情况下,会生成文件空洞,空洞部分不占用任何磁盘空间,并默认为 0。如果复制一个含空洞的文件,则新文件的空洞位置会占用磁盘空间 .. hint:: Intel x86 CPU 下 FreeBSD 的 :file:`/dev/kmem` 允许负的偏移量 读写数据 **************************************** 读写数据需要用到两个函数: .. code-block:: c ssize_t read(int fd, void* buf, size_t nbytes); ssize_t write(int fd, const void* buf, size_t nbytes); .. note:: 与 size_t 相比而言,ssize_t 允许返回负数 对于两个函数而言,参数 nbytes 代表了 buf 的大小。当函数成功时返回读出的字节数,失败时返回 -1。read 从文件向 buf 写数据,write 从 buf 读出数据 另外,以下情况会导致 read 返回的字节数少于 nbytes: - 已经到达文件末端 - 剩余数据不足 - 终端设备一般每次只允许读一行 - 面向记录的设备一次只允许读一个记录 - 信号中断 write 成功时返回值与 nbytes 相等,否则出错。一般 write 失败的原因是: - 磁盘已满 - 文件长度超出限制 当缓冲区大小与磁盘扇区大小相等时,磁盘的效率一般最高。 离散读和聚集写 **************************************** :abbr:`离散读 (Scatter Read)` 和 :abbr:`聚集写 (Gather Write)` 使得可以在一次函数调用中读写多个缓冲区,其函数签名为: .. code-block:: c ssize_t readv(int fd, const iovec* iov, int iovcnt); ssize_t writev(int fd, const iovec* iov, int iovcnt); 其中,iovec 的定义为: .. code-block:: c struct iovec{ void* iov_base; size_t iov_len; }; 也就是说 iov 实际上是一个数组的数组。参数 iovcnt 指明了 iov 的大小。两个函数都是依次对缓冲区进行访问,当一个缓冲区完全访问后才访问下一个。 使用轮询的非阻塞 IO **************************************** 我们可以看一下以轮询的方式查询如何从 stdin 读取数据: .. code-block:: c #include #include #include #include #include #include int main(int argc, char* argv[]) { int flags = fcntl(STDIN_FILENO, F_GETFL, 0); assert(flags >= 0); flags |= O_NONBLOCK; fcntl(STDIN_FILENO, F_SETFL, flags); char buf[255]; int nRead = 0; while(nRead != 2) { errno = 0; nRead = read(STDIN_FILENO, buf, 255); fprintf(stderr, "nRead = %d, errno = %d\n", nRead, errno); if(errno == EAGAIN) { puts("没有数据"); continue; } if(errno == 0) write(STDOUT_FILENO, buf, nRead); } return 0; } 当 stdin 没有数据时。read 返回 -1,error == EAGAIN。当没有数据时我们一直进行轮询。但是由于不清楚 stdin 到底有多少数据,因此我们规定当读取到的数据数量为 2 时跳出循环(换行符也计算在内) IO 多路复用 **************************************** select ======================================== select 是 IO 多路复用的一种 API,其典型特点是构造三个 1024 容量的数组,在其中保存了需要观察的文件描述符。每次调用 select 就会对这三个数组进行一次遍历。这三个数组分别是 readfds、writefds 和 exceptfds。另外,为了节省空间,这三个数组使用位图的形式实现。select 的 API 为: .. code-block:: c int select(int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, timeval* tvptr); 正如上述所言,readfds 用来储存需要读取的文件描述符,writefds 用来储存需要写入的文件描述符,exceptfds 用来储存发生异常的文件描述符,tvptr 用来表示 select 最高允许等待的时间。而 maxfdp 用来表示需要遍历的范围,其值为最大文件描述符加一,如果不指明的话,select 将会遍历 1024 的元素。 select 会返回已经准备好的描述符的数量,返回 -1 说明出错。描述符准备好的含义对于上述三个 fd_set 而言分别是是可读、可写、含未决异常。如果一个描述符已经准备好了读和写,那么会进行两次计数。 fd_set 是一个位图,对其修改需要使用特定的函数,这些函数根据实现可能被实现为宏: .. code-block:: c int FD_ISSET(int fd, fd_set* fdset); // 若 fd 在 fdset 中,返回非零值 int FD_CLR(int fd, fd_set* fdset); int FD_SET(int fd, fd_set* fdset); int FD_ZERO(fd_set* fdset); 当三个 fd_set 指针均为 NULL 时,select 提供了比 sleep 更加精细的休眠功能。而当 tvptr == NULL 时,则表明阻塞等待至有文件描述符准备好。 另外,select 还有一个变体 pselect 来提供更加精细的定时功能: .. code-block:: c int pselect(int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const timespec* tvptr, sigset_t* sigmask); 使用 sigmask 可以屏蔽指定信号 然后是使用 select 实现的非阻塞 IO: .. code-block:: c #include #include #include #include #include #include #include #include int main(int argc, char* argv[]) { int flags = fcntl(STDIN_FILENO, F_GETFL, 0); assert(flags >= 0); flags |= O_NONBLOCK; fcntl(STDIN_FILENO, F_SETFL, flags); fd_set readFds; fd_set excFds; FD_ZERO(&readFds); FD_ZERO(&excFds); char buf[255]; int ret; while(1) { bzero(buf, 255); FD_SET(STDIN_FILENO, &readFds); FD_SET(STDIN_FILENO, &excFds); ret = select(STDIN_FILENO + 1, &readFds, NULL, &excFds, NULL); assert(ret >= 0); if(FD_ISSET(STDIN_FILENO, &readFds)) { read(STDIN_FILENO, buf, 255); write(STDOUT_FILENO, buf, 255); } else if(FD_ISSET(STDIN_FILENO, &excFds)) { fprintf(stderr, "errno = %d", errno); } } return 0; } poll ======================================== poll 是另一种 IO 多路复用的手段,相比 select 最大支持 1024 个文件描述符而言,poll 支持的文件描述符是无穷的: .. code-block:: c int poll(pollfd* fdarray, nfds_t nfds, int timeout); .. note:: poll 底层使用链表储存 fd 其中,pollfd 的定义为: .. code-block:: c struct pollfd{ int fd; // 需要监控的文件描述符 short events; // 需要监控的事件 short revents; // 实际发生的事件 }; pollfd.revents 在 poll 返回时由内核更改,用户无需设定。 其中,events 为: +------------+--------------------+--------------------+------------------------------------+ | 标志 | 写入 events ? | 写入 revents ? | 描述 | +============+====================+====================+====================================+ | POLLIN | :math:`\checkmark` | :math:`\checkmark` | 可以不阻塞地读取高优先级以外的数据 | +------------+--------------------+--------------------+------------------------------------+ | POLLRDNORM | :math:`\checkmark` | :math:`\checkmark` | 可以不阻塞地读取普通数据 | +------------+--------------------+--------------------+------------------------------------+ | POLLRDBAND | :math:`\checkmark` | :math:`\checkmark` | 可以不阻塞地读取优先级数据 | +------------+--------------------+--------------------+------------------------------------+ | POLLPRI | :math:`\checkmark` | :math:`\checkmark` | 可以不阻塞地读取高优先级数据 | +------------+--------------------+--------------------+------------------------------------+ | POLLOUT | :math:`\checkmark` | :math:`\checkmark` | 可以不阻塞地写普通数据 | +------------+--------------------+--------------------+------------------------------------+ | POLLWRNORM | :math:`\checkmark` | :math:`\checkmark` | 同上 | +------------+--------------------+--------------------+------------------------------------+ | POLLWRBAND | :math:`\checkmark` | :math:`\checkmark` | 可以不阻塞地写优先级数据 | +------------+--------------------+--------------------+------------------------------------+ | POLLERR | | :math:`\checkmark` | 已出错 | +------------+--------------------+--------------------+------------------------------------+ | POLLHUP | | :math:`\checkmark` | 已挂断 | +------------+--------------------+--------------------+------------------------------------+ | POLLNVAL | | :math:`\checkmark` | 描述符没有引用一个文件 | +------------+--------------------+--------------------+------------------------------------+ 文件描述符被挂断后依然可读 事件与 select 略有不同: +--------+-------------------+ | 值 | 含义 | +========+===================+ | -1 | 永远等待 | +--------+-------------------+ | 0 | 立即返回 | +--------+-------------------+ | 非零值 | 等待 timeout 毫秒 | +--------+-------------------+ 使用 poll 实现的非阻塞 IO 方式为: .. code-block:: c #include #include #include #include #include #include #include int main(int argc, char* argv[]) { int flags = fcntl(STDIN_FILENO, F_GETFL, 0); assert(flags >= 0); flags |= O_NONBLOCK; fcntl(STDIN_FILENO, F_SETFL, flags); int nfds = 1; struct pollfd* pfds; pfds = calloc(nfds, sizeof(struct pollfd)); assert(pfds != NULL); pfds[0].fd = STDIN_FILENO; pfds[0].events |= POLLIN; while(1) { int ready; ready = poll(pfds, 1, -1); assert(ready != -1); fprintf(stderr, "Ready: %d\n", ready); char buf[255]; memset(buf, '\0', 255); for(int i = 0; i < nfds; ++i) { if(pfds[i].revents & POLLIN) { ssize_t l = read(pfds[i].fd, buf, 255); write(STDOUT_FILENO, buf, 255); } } } return 0; } epoll ======================================== :abbr:`epoll (Enhanced poll)` API 的行为与 poll 类似:监控多个文件描述符以查看其上可用的 IO。epoll 分为边缘触发和水平触发。 .. note:: - 另一方面,由于 select 和 poll 都是使用的轮询的方式,导致当文件描述符很多时性能较低,而 epoll 底层使用了红黑树来保证效率 - 更准确地来说,epoll 监控的是可读可写事件 epoll 的核心概念是 epoll示例:一个内核中数据结构,包含了两个列表: - 兴趣列表(也叫做 epoll set):包含了需要监控的文件描述符 - 就绪列表:包含了兴趣列表中已经就绪的文件描述符。就绪列表是内核根据当前 IO 状况动态创建的 以下 API 用于管理 epoll 实例: .. code-block:: c int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, epoll_event* event); int epoll_wait(int epfd, epoll_event* events, int maxevents, int timeout); epoll_create 用来创建一个 epoll 实例,并返回一个指向此实例的文件描述符 epoll_ctl 用来向 epoll 示例中添加、删除、更改感兴趣的文件描述符,op 的参数为: +---------------+-----------------------------------------------------------+ | 命令 | 描述 | +===============+===========================================================+ | EPOLL_CTL_ADD | 增加要监视的文件描述符 | +---------------+-----------------------------------------------------------+ | EPOLL_CTL_MOD | 更改目标文件描述符的事件 | +---------------+-----------------------------------------------------------+ | EPOLL_CTL_DEL | 删除要监视的文件描述符,event 参数会被忽略,可以传入 NULL | +---------------+-----------------------------------------------------------+ epoll_wait 在没有可用文件描述符时阻塞当前线程。当有可用文件描述符时将其写入 events 其中 epoll_event 的定义为: .. code-block:: c struct epoll_event { uint32_t events; // 与 poll 能监视的事件差不多,只是宏名前面加了个E epoll_data_t data; // 用户数据,除了能保存文件描述符以外,还能保存一些其它有关数据 }; typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; epoll_wait 用来等待文件描述符可用。当其成功返回时,可以通过 events->data->fd 拿到可用的文件描述符 水平触发和边缘触发 ======================================== 假设有以下事件发生: - 一个代表管道的文件描述符 rfd 被添加到兴趣列表 - 管道内在写端被写入了 2kB 数据 - epoll_wait 将 rfd 写入到就绪列表中 - 管道读端读了 1kB 的数据 - epoll_wait 再次被调用 则: - 如果 rfd 是边缘触发。则第五步时的就绪列表中不会包含 rfd,因为边缘触发只在文件描述符状态变化时才将其加入就绪列表 - 如果 rfd 是水平触发。则第五步时返回的就绪列表中仍会包含 rfd,因为水平触发会在文件描述符可用时将其加入就绪列表 .. important:: 边缘触发要求文件描述符是非阻塞的 使用 epoll 的非阻塞 IO 例子为: .. code-block:: c #include #include #include #include #include #include #include #include int main(int argc, char* argv[]) { int flags = fcntl(STDIN_FILENO, F_GETFL, 0); assert(flags >= 0); flags |= O_NONBLOCK; fcntl(STDIN_FILENO, F_SETFL, flags); int epfd = epoll_create1(0); assert(epfd != -1); struct epoll_event ev; struct epoll_event events[10]; ev.events = EPOLLIN | EPOLLET; ev.data.fd = STDIN_FILENO; int rtn = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); assert(rtn != -1); int nfds = 0; char buf[255]; while(1) { nfds = epoll_wait(epfd, events, 10, -1); assert(nfds != -1); for(size_t i = 0; i < nfds; ++i) { int fd = events[i].data.fd; bzero(buf, 255); read(fd, buf, 255); write(STDOUT_FILENO, buf, 255); } } return 0; } epoll 的五种写法 **************************************** 整理自 `6 种epoll的设计,让你吊打面试官,而且他不能还嘴`_ .. _`6 种epoll的设计,让你吊打面试官,而且他不能还嘴`: https://www.bilibili.com/video/BV1Dg411c7Tn epoll 有六种常用写法: - 单线程 accept,多线程 recv/send - 多线程 accept,多线程 recv/send 对于多线程而言,监听一个端口的方式是: .. code-block:: c listen(fd) pthread_create(thid, NULL, cd, &fd); pthread_create(thid, NULL, cd, &fd); pthread_create(thid, NULL, cd, &fd); pthread_create(thid, NULL, cd, &fd); 多线程处理同一个 listenfd,应当使用水平触发 多线程 Listen 的另一个问题是当链接进入时,会将所有的 listen 进程唤醒,解决的办法是在 epoll_wait 之前加锁。每次请求进入时只唤醒一个进程 - 多线程 epoll_wait,不区分 accept 和 recv/send - 多进程 epoll_wait,不区分 accept 和 recv/send - master 进程 accept,工作进程 recv/send .. note:: - 多进程适用于 session 是独立的,前后不关联的情况。比如即时通信是长链接,session 不是独立的就不推荐多进程模型 - Nginx 使用的是第三种模型 - 最后一种只是理论上,但是因为 fd 没法在进程间传递(fd 创建于 fork 之后),所以实际上没人用 mmap **************************************** mmap 可以将文件的一部分映射到内存中。之后程序对此内存的更改会由操作系统负责写回到磁盘上。尽管程序在写数据的时候可以超过映射文件的大小,但是超过的部分会被忽略掉。 .. hint:: mmap 将文件映射到进程的文件映射区,fork 的子进程会得到副本,但是执行 exec 的进程不会 .. code-block:: c void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length); mmap 将文件 fd 中的 [offset, offset + length] 部分映射到地址 addr 处。addr 如果置空则意味着让操作系统选择。函数的返回值即文件映射后指向的内存 .. hint:: 对于 Linux 而言,addr 是一个 hint 类型的参数。也就是说,mmap 不一定会将文件映射到 addr 处,一切应以返回值为准 prot 表明了映射区的权限,其权限不得超过 fd 拥有的权限: +------------+----------------+ | 命令 | 含义 | +============+================+ | PROT_READ | 映射区可读 | +------------+----------------+ | PROT_WRITE | 映射区可写 | +------------+----------------+ | PROT_EXEC | 映射区可执行 | +------------+----------------+ | PROT_NONE | 映射区不可访问 | +------------+----------------+ 另外,映射区的权限后面还可以使用 mprotect 更改: .. code-block:: c int mprotect(void *addr, size_t len, int prot); flags 指明了内核是否回写和是否进程共享的问题: +---------------------+--------------------------------------------------------------+ | 标志 | 含义 | +=====================+==============================================================+ | MAP_SHARED | 共享映射。对映射区的更改是进程间可见的,而且会回写到底层文件 | +---------------------+--------------------------------------------------------------+ | MAP_SHARED_VALIDATE | 与 MAP_SHARED 类似,但是会无效的 flag 会导致 mmap 失败 | +---------------------+--------------------------------------------------------------+ | MAP_PRIVATE | 对映射区的更改不会影响底层文件 | +---------------------+--------------------------------------------------------------+ .. important:: - 在进程结束时需要使用 munmap 取消映射 - 映射区的大小不能超过文件大小,超过部分会被忽略 - flags 在不同版本的内核中变化比较大,使用前应当先查阅手册 另外,你还可以通过 msync 强制系统将内容回写到文件: .. code-block:: c int msync(void *addr, size_t length, int flags); flags 的参数为: +---------------+--------------------------------------------+ | 标志 | 含义 | +---------------+--------------------------------------------+ | MS_ASYNC | 请求回写。函数会立即返回 | +---------------+--------------------------------------------+ | MS_SYNC | 请求阻塞至回写完成 | +---------------+--------------------------------------------+ | MS_INVALIDATE | 使其他人对此文件的映射失效(以便刷新文件) | +---------------+--------------------------------------------+ signalfd **************************************** TODO timerfd **************************************** TODO inotify **************************************** inotify 可以用来监控文件变化。但文件发生改变时,将会产生系统调用,inotify 通过 hack 这些系统调用来监控文件系统的变更。但是 inotify 不支持 FUSE 文件系统,更通俗地来讲是不支持网络文件系统和虚拟文件系统(例如 samba 挂载和 /proc 虚拟文件系统) 在 inotify-tools 软件包里提供了一些工具可以用来监控文件操作。例如如果我们打算监控 :file:`/srv/test` 文件上的操作,只需要执行 :: $ inotifywait -rme modify,attrib,move,close_write,create,delete,delete_self /srv/test .. seealso:: - `Inotify: 高效、实时的 Linux 文件系统事件监控框架 `_ - `Linux内核监控模块-3-系统调用的截获 - 守望丶麦田 - 博客园 `_ fanotify **************************************** fanotify 是 inotify 的更完善版本,其在 inotify 基础上添加了更多的功能,使得监听者实现了从“监听”到“监控”的跨越,同时也扩展了其应用的范围 .. seealso:: - `Linux文件事件监控之Fanotify [一] - 知乎 `_ - `一个Fanotify和FUSE配合使用导致的问题 - 知乎 `_